import os
import socket
import exceptions

import support
import strategies
import interaction

FULLNAME_FOR_LOCAL_USER_ON_LOCAL_MACHINE = "$"


class UserConfigurationMismatch(exceptions.Exception): pass
class UserStrategyAlreadyChosen(exceptions.Exception): pass
class StrategyDeterminationError(exceptions.Exception): pass


def _get_ip_from_hostname(host):
    return socket.getaddrinfo(host, None)[0][4][0]


class UserManager(support.Singleton):
    
    use_dry_run_strategies = property(lambda self: self.__use_dry_run_strategies)

    def __init__(self):
        super(UserManager, self).__init__()
        self.user_datas = {}
        # FIXME: Issue 21: encrypt these using a temporal key like md5(systime + random)?
        self.passwords_by_group = {}
        self.__use_dry_run_strategies = False
    
    def get_user_data(self, fullname):
        if fullname not in self.user_datas:
            self.user_datas[fullname] = UserData(fullname)
        return self.user_datas[fullname]
    
    def get_password_for_group(self, group):
        if group not in self.passwords_by_group:
            users_in_group = self.find_users_in_password_group(group)
            fullnames = [user.fullname for user in users_in_group]
            prompt = "%s\nPassword: " % "\n".join(fullnames)
            self.passwords_by_group[group] = interaction.IOController.get_instance().prompt_for_password(prompt)
        return self.passwords_by_group[group]
    
    def find_users_in_password_group(self, password_group):
        user_fullnames = []
        for fullname in self.user_datas:
            user_data = self.user_datas[fullname]
            if user_data.password_group == password_group:
                user_fullnames.append(user_data.fullname)
        user_fullnames = list(set(user_fullnames))
        return [User(fullname) for fullname in user_fullnames]
    
    def force_all_users_to_use_dry_run_strategies(self):
        self.__use_dry_run_strategies = True


class UserData(object):
    
    def _get_ip(self):
        if not self.__ip:
            if self.host != "localhost":
                self.__ip = _get_ip_from_hostname(self.host)
            else:
                self.__ip = None
        return self.__ip
        
    def _get_detail(self):
        if not self.__detail:
            if self.host != "localhost":
                self.__detail = self.ip
            else:
                self.__detail = "locally on %s" % socket.gethostname()
        return self.__detail
    
    ip = property(_get_ip)
    detail = property(_get_detail)
    
    def __init__(self, fullname):
                
        if fullname == FULLNAME_FOR_LOCAL_USER_ON_LOCAL_MACHINE:
            self.name = os.getlogin()
            self.host = "localhost"
            self.fullname = "%s@localhost" % self.name
        else:
            pieces = fullname.split("@")
            if len(pieces) != 2:
                raise ValueError("'%s' is not a valid, fully-qualified username (e.g. user@example.com)")
            self.name = pieces[0]
            self.host = pieces[1]
            self.fullname = fullname
            
        if self.host == "localhost":
            self.password_group = "autogroup:localhost"
        else:
            self.password_group = None
        self.su_from = None
        
        # lazy-loaded properties
        self.__ip = None
        self.__detail = None
        

class User(object):

    def _get_password(self):
        return UserManager.get_instance().get_password_for_group(self.data.password_group)

    def _get_strategy(self):
        if not self._strategy:
            
            if UserManager.get_instance().use_dry_run_strategies:
                
                # we're doing dry execution
                self._strategy = strategies.DryRunStrategy(self)
            
            else:
                
                # we must smartly choose a strategy
                def pick_proper_local_strategy(user):
                    # FIXME: Issue 12: modify selection based on su_from, and who the current user is (comparing with os.getlogin())
                    return strategies.LocalExpecterStrategy(user)

                def pick_proper_remote_strategy(user):
                    if user.su_from:
                        return strategies.RemoteSuExpecterStrategy(user)
                    else:
                        return strategies.RemoteExpecterStrategy(user)

                # determine the proper strategy for this user
                if self.host == "localhost":
                    self._strategy = pick_proper_local_strategy(self)

                else:

                    # determine whether we should run locally or not
                    try:
                        user_host_ip = _get_ip_from_hostname(self.host)   
                        this_host = socket.gethostname()
                        this_ip = _get_ip_from_hostname(this_host)
                        if this_ip == user_host_ip:
                            self._strategy = pick_proper_local_strategy(self)
                        else:
                            self._strategy = pick_proper_remote_strategy(self)
                    except socket.gaierror:
                        self._strategy = pick_proper_remote_strategy(self)

                # cache the strategy
                if not self._strategy:
                    raise StrategyDeterminationError("unable to determine the proper strategy for '%s'" % self.fullname)

        # return the strategy for this user object
        return self._strategy
    
    def _get_su_from(self):
        if self.data.su_from:
            return User(self.data.su_from)
        else:
            return None

    name = property(lambda self: self.data.name)
    host = property(lambda self: self.data.host)
    port = property(lambda self: 22) # TODO: allow specification of alternate ports
    ip = property(lambda self: self.data.ip)
    detail = property(lambda self: self.data.detail)
    fullname = property(lambda self: self.data.fullname)
    su_from = property(_get_su_from)
    password_group = property(lambda self: self.data.password_group)
    password = property(_get_password)
    strategy = property(_get_strategy)
    
    def __init__(self, fullname, su_from = None):
        self.data = UserManager.get_instance().get_user_data(fullname)
        self._strategy = None
    
    def __eq__(self,other):
        if isinstance(other,User):
            return self.data.name == other.data.name
        return False
    
    def __ne__(self,other):
        return not __eq__(other)
    
    def __hash__(self):
        return hash(self.data.fullname)
    
    def configure(self, password_group = None, su_from = None):
        
        if self._strategy:
            
            raise UserStrategyAlreadyChosen("the user '%s' has already initialized its internal strategy, configuration changes are not allowed" % self.fullname)

        else:
            # password_group setting
            if password_group:
                if self.data.password_group and self.data.password_group != password_group:
                    raise UserConfigurationMismatch("conflicting password_group configuration detected ('%s' != '%s')" % (password_group, self.data.password_group))
                else:
                    self.data.password_group = password_group
        
            # su_from setting
            if su_from:
                if self.data.su_from and self.data.su_from != su_from:
                    raise UserConfigurationMismatch("conflicting su_from configuration detected ('%s' != '%s')" % (su_from, self.data.su_from))
                else:
                    # if no hostname was specified, assume same host
                    if "@" not in su_from:
                        su_from = "%s@%s" % (su_from, self.host)
                    
                    # get the actual user
                    su_from_user = User(su_from)
                    
                    # ensure that the hosts match
                    if su_from_user.host != self.host:
                        raise UserConfigurationMismatch("user '%s' must su_from a user on the same machine ('%s' is on another machine)" % (self.fullname, su_from_user.fullname))
                    else:
                        # everything's kosher, assign it
                        self.data.su_from = su_from
        
        return self

